-
Notifications
You must be signed in to change notification settings - Fork 61
fix: flickering of active collaborator icons between states #3951
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: flickering of active collaborator icons between states #3951
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #3951 +/- ##
=======================================
Coverage 88.83% 88.84%
=======================================
Files 422 422
Lines 19118 19118
=======================================
+ Hits 16984 16985 +1
+ Misses 2134 2133 -1 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
suggestion from @stuartc, "use phoenix channel callback instead of timeouts to keep awareness active". |
561dffc to
43b4845
Compare
|
update
|
elias-ba
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @doc-han! The store implementation and caching mechanism look great. However, I noticed the ActiveCollaborators.tsx component wasn't updated to use the new 12-second threshold. The component still uses lessthanmin(user.lastSeen, 2) on line 47, which is probably causing the JavaScript tests to fail.
stuartc
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have concerns about the core approach here. This implementation
conflates the Yjs awareness heartbeat (keeping the connection alive)
with user presence (activity/engagement). The result is that user
presence state is now driven by each browser's internal throttling
policies rather than actual user behavior. What's happening:
- The frozen timestamp technique keeps awareness alive during throttling
- But we're then using that same lastSeen timestamp to determine if
users are "active" or "inactive" - This means presence state is an indirect side-effect of browser
throttling, not a direct measurement of user engagement
The constraints this creates:
- Browser throttles → lastSeen updates slow → appears inactive
- Browser active → lastSeen updates normally → appears active
We're treating the browser's power-saving policies as a signal for user
presence. This varies by: Browser, device & OS, battery state, power
profile...
Extensibility problem:
Where would we add actual presence detection?
If we wanted to:
- Check document.visibilityState directly
- Track user interactions (mouse, keyboard)
- Distinguish "tab switched for a few seconds" from "user left computer"
Would we keep extending lastSeen? That field is already serving two
purposes:
- Keep Yjs awareness alive/changing (its original job)
- Indicate user activity (what we're overloading it with)
We discussed the other day: Heartbeat and presence are separate
concerns. The heartbeat exists to prevent connection timeouts - it's
plumbing. User presence is a product feature about engagement/activity.
Mixing them means we can't adjust one without affecting the other.
Example: If we want to show users as inactive faster (say 5 seconds), we
can't - we're locked to the 10-30s window by Yjs's 30-second timeout. If
we want to keep users visible longer after they close a tab, we add
caching layers that delay all removals by 60+ seconds.
The 60-second removal time: Users disappear 60-70 seconds after closing
a tab because we need the cache to smooth over throttling-induced
flickering. But that's a workaround for using throttling as a presence
signal in the first place.
Request: What would it take to separate these? Could we: Use lastSeen
only for connection health (keep current 10s updates) Add a separate
lastActivity and/or lastState field to awareness (active, away, idle) +
timestamp
Update that field based on visibility API, not throttling side-effects
Let the UI read lastActivity and/or lastState instead of calculating
from lastSeen This would give us direct control over presence without
fighting browser throttling behavior. The frozen timestamp technique
might not be needed since it's value only needs to actually change in
order to keep the awareness around. But wouldn't drive the user-facing
presence indicator. I want to understand: what blocked this approach? Is
there a technical constraint I'm missing, or was the lastSeen
overloading the simpler path?
|
@stuartc I think I understand clearly what you mean.
don't we still need the caching to smoothen out when thinking... |
Yeah I agree, we still need the smoothing. But we can use logic like I think |
|
For the consolidation of multiple tabs open by a user. I already have that in place but I've changed how the priority is picked in the other PR. |
|
with this said. on #4053
This provides a better user experience than before. thinking... What do you think about this and the whole caching stuff in general. |
|
Closing this PR soon in favour of #4053 |
Implement 60-second cache in awareness store to stabilize collaborator
states and prevent icon flickering between active/inactive/unavailable
states during rapid awareness updates.
Key changes:
- Add userCache Map to AwarenessState with 60s TTL
- Cache users as they disconnect from awareness, showing them as inactive
- Clean up expired cache entries every 30 seconds
- Update ActiveCollaborators to use cached users for stability
- Fix inactive collaborators to continue pinging their last seen timestamp
- Add comprehensive tests for cache behavior and timing
Technical details:
- userCache stores { user, cachedAt } tuples indexed by userId
- When user disconnects from awareness (no longer in cursorsMap), they
remain in the users array via cache for 60s before being removed
- Page visibility API integration ensures lastSeen updates continue
even when page is hidden (prevents premature cache expiration)
- Periodic cleanup prevents memory leaks from stale cache entries
Fixes #3931
Co-authored-by: Farhan Yahaya <yahyafarhan48@gmail.com>
Implement a new unified useAwareness() hook that consolidates
useRemoteUsers(), useLiveRemoteUsers(), and useUserCursors() into
a single flexible API.
Key changes:
- Remove excludeLocal option - hook always excludes local user
- Support cached mode for smooth avatar transitions (60s TTL)
- Support map format for efficient Monaco cursor CSS generation
- Add comprehensive test coverage (26 tests)
API:
- useAwareness() - live remote users (for cursors)
- useAwareness({ cached: true }) - cached remote users (for avatars)
- useAwareness({ format: 'map' }) - Map format (for Monaco CSS)
Legacy hooks remain available but are marked as deprecated.
Migrate components to unified useAwareness() API
Migrate all collaborative editor components to use the new
simplified useAwareness() hook and update test mocks.
Component migrations:
- ActiveCollaborators: useRemoteUsers() → useAwareness({ cached: true })
- Cursors: useUserCursors() → useAwareness({ format: 'map' })
- CollaborationWidget: useAwarenessUsers() → useAwareness({ cached: true })
- RemoteCursor: useRemoteUsers() → useAwareness()
Test updates:
- Update mocks to use useAwareness instead of legacy hooks
UX improvements:
- CollaborationWidget now shows "You + N others" for clarity
- RemoteCursor bug fix: cursors now disappear immediately on disconnect
Optimize remote cursor rendering to prevent jumpy movement
Fixes browser-specific issue where remote cursors appeared jumpy when
viewport updates were frequent (during panning/zooming). The problem
was render storms interrupting CSS transitions.
Changes:
- Add position rounding to reduce micro-pixel updates
- Replace left/top positioning with CSS transform for better GPU acceleration
- Wrap RemoteCursor in React.memo with custom comparison to skip re-renders
when position changes are <1px
This prevents frequent viewport updates from recreating cursor elements
and interrupting their CSS transitions.
d61143f to
2483a1e
Compare
Description
Implements active, inactive and unavailable states for active collaborators on a workflow.
active - a user is active when a workflow is open and the user is actively on the tab.
inactive - a user is inactive after 12s if they leave the tab where the workflow is open.
unavailable - a user is removed from active collaborators after approx. 1min of closing the tab where the workflow is open.
Main issue
[a] Throttling of browser timers is our biggest villain in this PR.
What problems does it cause?
Hence, when a user leaves a tab, they will be marked as offline when throttling kicks in, and when after being throttled, their event finally goes through, they then get marked back as online. making them flicker
[b] Another thing to be aware of 10secs interval
Every 10secs we actually update the user's
lastSeentoDate.now()but then when the user leaves the tab, we don't update thislastSeenvalue. And because there's no state change/diff happening when the user leaves the tab. No event is actually triggered to keep ydoc live.Approach in this PR
When user leaves the tab. for every 10secs, we increment lastSeen by 1ms which will trigger an event to keep ydoc live. This is to battle [b]
We have a small cache for activeCollaborators that lives for 60secs. when a user shows up as an active-collaborator we keep him for 60secs max. this 60secs is renewed when they show up again in the new set of active collaborators we receive. This resolves [a]
and 2. work hand in hand to smoothen the active collaborators regardless of the ydoc and browser throttling limits.
Side Effect
When a user closes a tab or a workflow. They disappear from other users active-collaborators after 60secs. but users would know inactive collaborators real quick(12secs)
Closes #3931
Validation steps
Note: Some values got changed.
eg.
Additional notes for the reviewer
AI Usage
Please disclose how you've used AI in this work (it's cool, we just want to know!):
You can read more details in our Responsible AI Policy
Pre-submission checklist
:owner,:admin,:editor,:viewer)